#!/usr/bin/env python3
"""
调度器服务
独立的APScheduler服务，提供任务调度功能
"""

import os
import sys
import json
import time
import signal
import platform
from datetime import datetime
from flask import Flask, request, jsonify
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from apscheduler.events import EVENT_JOB_EXECUTED, EVENT_JOB_ERROR
from datetime import datetime
import requests
import logging
from logging.handlers import RotatingFileHandler
import pytz

# 设置默认时区
TIMEZONE = pytz.timezone('Asia/Shanghai')

# 设置时区（仅在Unix/Linux系统上可用）
if platform.system() != 'Windows':
    time.tzset()

# 添加项目根目录到路径
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from models import db, ScheduledTask, CustomTask

# 配置日志
if not os.path.exists('logs'):
    os.mkdir('logs')
# 检查是否通过start_services.py启动（会有标准输出重定向到日志文件）
is_started_by_service_manager = sys.stdout.name != '<stdout>'
if not is_started_by_service_manager:
    log_date = datetime.now().strftime('%Y-%m-%d')
    file_handler = RotatingFileHandler(f'logs/scheduler_service_{log_date}.log', maxBytes=10240, backupCount=10)
    file_handler.setFormatter(logging.Formatter(
        '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
    ))
    file_handler.setLevel(logging.INFO)
else:
    # 如果是通过服务管理器启动，则不创建额外的日志文件
    file_handler = logging.StreamHandler()
    file_handler.setFormatter(logging.Formatter(
        '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
    ))
    file_handler.setLevel(logging.INFO)

app = Flask(__name__)

# 数据库配置 - 使用外部MySQL
import sys
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
from config.mysql_config import MySQLConfig
mysql_config = MySQLConfig()
mysql_config.validate_config()

app.config['SQLALCHEMY_DATABASE_URI'] = mysql_config.get_database_uri()
app.config['SQLALCHEMY_ENGINE_OPTIONS'] = mysql_config.get_engine_options()
app.logger.info(f"调度器服务使用MySQL数据库: {mysql_config.MYSQL_HOST}:{mysql_config.MYSQL_PORT}/{mysql_config.MYSQL_DATABASE}")

app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

# 服务配置
EXECUTOR_SERVICE_URL = os.environ.get('EXECUTOR_SERVICE_URL', 'http://localhost:5002')

# 初始化数据库
db.init_app(app)

# 配置日志
app.logger.addHandler(file_handler)
app.logger.setLevel(logging.INFO)

# 全局调度器
dingtalk_scheduler = None
custom_scheduler = None

def init_schedulers():
    """初始化调度器"""
    global dingtalk_scheduler, custom_scheduler
    
    # 创建钉钉消息任务调度器
    dingtalk_scheduler = BackgroundScheduler(timezone=TIMEZONE)
    
    # 创建用户自定义定时任务调度器
    custom_scheduler = BackgroundScheduler(timezone=TIMEZONE)
    
    # 添加事件监听器
    dingtalk_scheduler.add_listener(job_listener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR)
    custom_scheduler.add_listener(job_listener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR)
    
    app.logger.info("调度器初始化完成")

def handle_task_failure(task, error_msg):
    """处理任务执行失败"""
    task.last_error = error_msg
    task.retry_count += 1
    
    if task.retry_count <= task.max_retries:
        # 计算下次重试时间
        next_run = datetime.now().timestamp() + task.retry_delay
        
        # 获取对应的调度器和任务ID
        if isinstance(task, ScheduledTask):
            scheduler = dingtalk_scheduler
            job_id = f'dingtalk_task_{task.id}'
            task_type = '钉钉任务'
        else:
            scheduler = custom_scheduler
            job_id = f'custom_task_{task.id}'
            task_type = '自定义任务'
        
        # 添加重试任务
        scheduler.add_job(
            func=execute_dingtalk_task if isinstance(task, ScheduledTask) else execute_custom_task,
            trigger='date',
            run_date=datetime.fromtimestamp(next_run),
            args=[task.id],
            id=f'{job_id}_retry_{task.retry_count}',
            replace_existing=True
        )
        
        app.logger.info(f'{task_type} {task.id} 将在 {task.retry_delay} 秒后进行第 {task.retry_count} 次重试')
    else:
        app.logger.error(f'{task_type if "task_type" in locals() else "任务"} {task.id} 已达到最大重试次数 {task.max_retries}')
    
    db.session.commit()

def job_listener(event):
    """任务执行监听器"""
    with app.app_context():
        # 从任务ID中解析出任务类型和ID
        job_id = event.job_id
        is_retry = '_retry_' in job_id
        
        if job_id.startswith('dingtalk_task_'):
            task_id = int(job_id.split('_')[2] if not is_retry else job_id.split('_')[2])
            task = ScheduledTask.query.get(task_id)
            task_type = '钉钉任务'
        elif job_id.startswith('custom_task_'):
            task_id = int(job_id.split('_')[2] if not is_retry else job_id.split('_')[2])
            task = CustomTask.query.get(task_id)
            task_type = '自定义任务'
        else:
            app.logger.error(f'未知任务类型: {job_id}')
            return
        
        if not task:
            app.logger.error(f'{task_type} {task_id} 不存在')
            return
        
        if event.exception:
            retry_info = f'(第 {task.retry_count} 次重试)' if is_retry else ''
            app.logger.error(f'{task_type} {task_id} {retry_info} 执行失败: {event.exception}')
        else:
            retry_info = f'(重试成功)' if is_retry else ''
            app.logger.info(f'{task_type} {task_id} {retry_info} 执行成功')
            
            # 如果是重试成功，重置重试计数
            if is_retry:
                task.retry_count = 0
                task.last_error = None
                db.session.commit()

def execute_dingtalk_task(task_id):
    """执行钉钉任务"""
    with app.app_context():
        task = ScheduledTask.query.get(task_id)
        if not task:
            app.logger.error(f'钉钉任务 {task_id} 不存在')
            return
        
        try:
            # 调用执行器服务
            response = requests.post(f"{EXECUTOR_SERVICE_URL}/api/tasks/{task_id}/execute")
            if response.status_code == 200:
                app.logger.info(f'钉钉任务 {task_id} 执行成功')
                # 重置重试计数
                task.retry_count = 0
                task.last_error = None
                db.session.commit()
            else:
                error_msg = response.text
                app.logger.error(f'钉钉任务 {task_id} 执行失败: {error_msg}')
                handle_task_failure(task, error_msg)
        except Exception as e:
            error_msg = str(e)
            app.logger.error(f'钉钉任务 {task_id} 执行异常: {error_msg}')
            handle_task_failure(task, error_msg)

def execute_custom_task(task_id):
    """执行自定义任务"""
    with app.app_context():
        task = CustomTask.query.get(task_id)
        if not task:
            app.logger.error(f'自定义任务 {task_id} 不存在')
            return
        
        try:
            # 调用执行器服务
            response = requests.post(f"{EXECUTOR_SERVICE_URL}/api/custom_tasks/{task_id}/execute")
            if response.status_code == 200:
                app.logger.info(f'自定义任务 {task_id} 执行成功')
                # 重置重试计数
                task.retry_count = 0
                task.last_error = None
                db.session.commit()
            else:
                error_msg = response.text
                app.logger.error(f'自定义任务 {task_id} 执行失败: {error_msg}')
                handle_task_failure(task, error_msg)
        except Exception as e:
            error_msg = str(e)
            app.logger.error(f'自定义任务 {task_id} 执行异常: {error_msg}')
            handle_task_failure(task, error_msg)

def load_tasks():
    """从数据库加载任务"""
    with app.app_context():
        # 清除现有任务
        if dingtalk_scheduler:
            for job in dingtalk_scheduler.get_jobs():
                dingtalk_scheduler.remove_job(job.id)
        
        if custom_scheduler:
            for job in custom_scheduler.get_jobs():
                custom_scheduler.remove_job(job.id)
        
        # 加载钉钉任务
        dingtalk_tasks = ScheduledTask.query.filter_by(is_active=True).all()
        for task in dingtalk_tasks:
            try:
                cron_parts = task.cron_expression.split()
                if len(cron_parts) == 5:
                    minute, hour, day, month, day_of_week = cron_parts
                    
                    dingtalk_scheduler.add_job(
                        func=execute_dingtalk_task,
                        trigger=CronTrigger(
                            minute=minute,
                            hour=hour,
                            day=day,
                            month=month,
                            day_of_week=day_of_week
                        ),
                        args=[task.id],
                        id=f'dingtalk_task_{task.id}',
                        replace_existing=True
                    )
                    app.logger.info(f'添加钉钉任务: {task.name} (ID: {task.id}, cron: {task.cron_expression})')
            except Exception as e:
                app.logger.error(f'添加钉钉任务失败: {task.name}, 错误: {str(e)}')
        
        # 加载自定义任务
        custom_tasks = CustomTask.query.filter_by(is_active=True).all()
        for task in custom_tasks:
            try:
                cron_parts = task.cron_expression.split()
                if len(cron_parts) == 5:
                    minute, hour, day, month, day_of_week = cron_parts
                    
                    custom_scheduler.add_job(
                        func=execute_custom_task,
                        trigger=CronTrigger(
                            minute=minute,
                            hour=hour,
                            day=day,
                            month=month,
                            day_of_week=day_of_week
                        ),
                        args=[task.id],
                        id=f'custom_task_{task.id}',
                        replace_existing=True
                    )
                    app.logger.info(f'添加自定义任务: {task.name} (ID: {task.id}, cron: {task.cron_expression})')
            except Exception as e:
                app.logger.error(f'添加自定义任务失败: {task.name}, 错误: {str(e)}')

def start_schedulers():
    """启动调度器"""
    global dingtalk_scheduler, custom_scheduler
    
    if dingtalk_scheduler and not dingtalk_scheduler.running:
        dingtalk_scheduler.start()
        app.logger.info("钉钉消息任务调度器已启动")
    
    if custom_scheduler and not custom_scheduler.running:
        custom_scheduler.start()
        app.logger.info("用户自定义定时任务调度器已启动")

def stop_schedulers(wait=True):
    """停止调度器
    
    Args:
        wait (bool, optional): 是否等待当前正在执行的任务完成。默认为True。
    """
    global dingtalk_scheduler, custom_scheduler
    
    try:
        # 先移除所有任务，防止在关闭过程中有新任务提交
        if dingtalk_scheduler and dingtalk_scheduler.running:
            for job in dingtalk_scheduler.get_jobs():
                dingtalk_scheduler.remove_job(job.id)
            app.logger.info("已清除钉钉消息任务调度器中的所有任务")
        
        if custom_scheduler and custom_scheduler.running:
            for job in custom_scheduler.get_jobs():
                custom_scheduler.remove_job(job.id)
            app.logger.info("已清除用户自定义定时任务调度器中的所有任务")
        
        # 然后关闭调度器
        if dingtalk_scheduler and dingtalk_scheduler.running:
            dingtalk_scheduler.shutdown(wait=wait)
            app.logger.info("钉钉消息任务调度器已停止")
        
        if custom_scheduler and custom_scheduler.running:
            custom_scheduler.shutdown(wait=wait)
            app.logger.info("用户自定义定时任务调度器已停止")
    except Exception as e:
        app.logger.error(f"停止调度器时发生错误: {str(e)}")
        # 即使出错，也要尝试关闭调度器
        if dingtalk_scheduler and dingtalk_scheduler.running:
            try:
                dingtalk_scheduler.shutdown(wait=False)
            except:
                pass
        if custom_scheduler and custom_scheduler.running:
            try:
                custom_scheduler.shutdown(wait=False)
            except:
                pass

# API路由
@app.route('/status')
def get_status():
    """获取调度器状态"""
    global dingtalk_scheduler, custom_scheduler
    
    status = {
        'dingtalk_scheduler': {
            'running': dingtalk_scheduler.running if dingtalk_scheduler else False,
            'job_count': len(dingtalk_scheduler.get_jobs()) if dingtalk_scheduler else 0,
            'jobs': []
        },
        'custom_scheduler': {
            'running': custom_scheduler.running if custom_scheduler else False,
            'job_count': len(custom_scheduler.get_jobs()) if custom_scheduler else 0,
            'jobs': []
        }
    }
    
    # 获取任务详情
    if dingtalk_scheduler:
        for job in dingtalk_scheduler.get_jobs():
            try:
                next_run = job.next_run_time
                status['dingtalk_scheduler']['jobs'].append({
                    'id': job.id,
                    'trigger': str(job.trigger),
                    'next_run': next_run.isoformat() if next_run else None
                })
            except AttributeError:
                status['dingtalk_scheduler']['jobs'].append({
                    'id': job.id,
                    'trigger': str(job.trigger),
                    'next_run': None
                })
    
    if custom_scheduler:
        for job in custom_scheduler.get_jobs():
            try:
                next_run = job.next_run_time
                status['custom_scheduler']['jobs'].append({
                    'id': job.id,
                    'trigger': str(job.trigger),
                    'next_run': next_run.isoformat() if next_run else None
                })
            except AttributeError:
                status['custom_scheduler']['jobs'].append({
                    'id': job.id,
                    'trigger': str(job.trigger),
                    'next_run': None
                })
    
    return jsonify(status)

@app.route('/api/health')
def health_check():
    """健康检查端点"""
    return jsonify({'status': 'ok'})

@app.route('/api/reload', methods=['POST'])
def reload_scheduler():
    """重新加载调度器"""
    try:
        app.logger.info("开始重新加载调度器...")
        
        # 获取请求参数，是否等待任务完成
        wait = request.json.get('wait', True) if request.is_json else True
        app.logger.info(f"重新加载调度器，wait参数为: {wait}")
        
        try:
            # 停止调度器
            app.logger.info("正在停止调度器...")
            stop_schedulers(wait=wait)
            app.logger.info("调度器已停止")
        except Exception as stop_error:
            app.logger.error(f"停止调度器时发生错误: {str(stop_error)}")
            # 继续执行，尝试重新初始化调度器
        
        # 等待停止完成
        app.logger.info("等待调度器完全停止...")
        time.sleep(5)  # 增加等待时间，确保调度器完全停止
        
        # 重新初始化调度器
        app.logger.info("重新初始化调度器...")
        global dingtalk_scheduler, custom_scheduler
        
        # 确保旧的调度器被完全清理
        dingtalk_scheduler = None
        custom_scheduler = None
        
        # 初始化新的调度器
        dingtalk_scheduler = BackgroundScheduler(timezone=TIMEZONE)
        custom_scheduler = BackgroundScheduler(timezone=TIMEZONE)
        
        # 添加日志监听器
        dingtalk_scheduler.add_listener(job_listener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR)
        custom_scheduler.add_listener(job_listener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR)
        
        # 重新加载任务
        app.logger.info("正在加载任务...")
        load_tasks()
        app.logger.info("任务加载完成")
        
        # 启动调度器
        app.logger.info("正在启动调度器...")
        start_schedulers()
        app.logger.info("调度器已启动")
        
        app.logger.info("调度器重新加载完成")
        return jsonify({'success': True, 'message': '调度器重新加载成功'})
    except Exception as e:
        app.logger.error(f"重新加载调度器失败: {str(e)}")
        return jsonify({'success': False, 'error': str(e)}), 500

@app.route('/api/tasks', methods=['GET'])
def get_tasks():
    """获取任务列表"""
    with app.app_context():
        dingtalk_tasks = ScheduledTask.query.all()
        custom_tasks = CustomTask.query.all()
        
        tasks = []
        for task in dingtalk_tasks:
            # 获取下一次执行时间
            next_run_time = None
            if task.is_active and dingtalk_scheduler:
                job = dingtalk_scheduler.get_job(f'dingtalk_task_{task.id}')
                if job and job.next_run_time:
                    next_run_time = job.next_run_time.isoformat()
            
            tasks.append({
                'id': task.id,
                'name': task.name,
                'type': 'dingtalk',
                'cron_expression': task.cron_expression,
                'is_active': task.is_active,
                'status': task.status,
                'last_run_time': task.last_run_time.isoformat() if task.last_run_time else None,
                'last_run_status': task.last_run_status,
                'next_run_time': next_run_time
            })
        
        for task in custom_tasks:
            # 获取下一次执行时间
            next_run_time = None
            if task.is_active and custom_scheduler:
                job = custom_scheduler.get_job(f'custom_task_{task.id}')
                if job and job.next_run_time:
                    next_run_time = job.next_run_time.isoformat()
            
            tasks.append({
                'id': task.id,
                'name': task.name,
                'type': 'custom',
                'cron_expression': task.cron_expression,
                'is_active': task.is_active,
                'status': task.status,
                'last_run_time': task.last_run_time.isoformat() if task.last_run_time else None,
                'last_run_status': task.last_run_status,
                'next_run_time': next_run_time
            })
        
        return jsonify(tasks)

@app.route('/api/tasks', methods=['POST'])
def create_task():
    """创建任务"""
    try:
        data = request.json
        task_type = data.get('type')
        
        with app.app_context():
            if task_type == 'dingtalk':
                task = ScheduledTask(
                    name=data['name'],
                    cron_expression=data['cron_expression'],
                    script_path=data['script_path'],
                    params=data.get('params'),
                    user_id=data['user_id']
                )
                db.session.add(task)
            elif task_type == 'custom':
                task = CustomTask(
                    name=data['name'],
                    cron_expression=data['cron_expression'],
                    script_id=data['script_id'],
                    user_id=data['user_id']
                )
                db.session.add(task)
            else:
                return jsonify({'error': '无效的任务类型'}), 400
            
            db.session.commit()
            
            # 重新加载调度器
            load_tasks()
            
            return jsonify({'success': True, 'task_id': task.id})
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/api/tasks/<int:task_id>', methods=['PUT'])
def update_task(task_id):
    """更新任务"""
    try:
        data = request.json
        
        with app.app_context():
            # 先尝试钉钉任务
            task = ScheduledTask.query.get(task_id)
            if not task:
                # 再尝试自定义任务
                task = CustomTask.query.get(task_id)
            
            if not task:
                return jsonify({'error': '任务不存在'}), 404
            
            # 更新任务属性
            for key, value in data.items():
                if hasattr(task, key):
                    setattr(task, key, value)
            
            db.session.commit()
            
            # 重新加载调度器
            load_tasks()
            
            return jsonify({'success': True})
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/api/tasks/<int:task_id>', methods=['DELETE'])
def delete_task(task_id):
    """删除任务"""
    try:
        with app.app_context():
            # 先尝试钉钉任务
            task = ScheduledTask.query.get(task_id)
            if not task:
                # 再尝试自定义任务
                task = CustomTask.query.get(task_id)
            
            if not task:
                return jsonify({'error': '任务不存在'}), 404
            
            db.session.delete(task)
            db.session.commit()
            
            # 重新加载调度器
            load_tasks()
            
            return jsonify({'success': True})
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/api/tasks/<int:task_id>/toggle', methods=['POST'])
def toggle_task(task_id):
    """切换任务状态"""
    try:
        with app.app_context():
            # 先尝试钉钉任务
            task = ScheduledTask.query.get(task_id)
            if not task:
                # 再尝试自定义任务
                task = CustomTask.query.get(task_id)
            
            if not task:
                return jsonify({'error': '任务不存在'}), 404
            
            task.is_active = not task.is_active
            db.session.commit()
            
            # 重新加载调度器
            load_tasks()
            
            return jsonify({'success': True, 'is_active': task.is_active})
    except Exception as e:
        return jsonify({'error': str(e)}), 500

def signal_handler(signum, frame):
    """信号处理器"""
    app.logger.info("收到停止信号，正在关闭调度器...")
    stop_schedulers()
    sys.exit(0)

def apply_retry_fields_migration():
    """应用重试字段迁移 - MySQL版本"""
    try:
        with app.app_context():
            from sqlalchemy import text, inspect
            
            # 获取数据库检查器
            inspector = inspect(db.engine)
            
            # 检查scheduled_task表是否已经存在重试相关字段
            scheduled_task_columns = inspector.get_columns('scheduled_task')
            scheduled_task_column_names = [col['name'] for col in scheduled_task_columns]
            
            # 检查custom_task表是否已经存在重试相关字段
            custom_task_columns = inspector.get_columns('custom_task')
            custom_task_column_names = [col['name'] for col in custom_task_columns]
            
            scheduled_task_needs_migration = 'max_retries' not in scheduled_task_column_names
            custom_task_needs_migration = 'max_retries' not in custom_task_column_names
            
            if not scheduled_task_needs_migration and not custom_task_needs_migration:
                app.logger.info("两个表的重试相关字段都已存在，无需重复应用")
                return True
            
            # 添加scheduled_task表的重试相关字段
            if scheduled_task_needs_migration:
                app.logger.info("正在添加scheduled_task表的重试相关字段...")
                db.session.execute(text("ALTER TABLE scheduled_task ADD COLUMN max_retries INTEGER DEFAULT 3"))
                db.session.execute(text("ALTER TABLE scheduled_task ADD COLUMN retry_delay INTEGER DEFAULT 300"))
                db.session.execute(text("ALTER TABLE scheduled_task ADD COLUMN retry_count INTEGER DEFAULT 0"))
                db.session.execute(text("ALTER TABLE scheduled_task ADD COLUMN last_error TEXT"))
                app.logger.info("✅ scheduled_task表字段添加完成")
            
            # 添加custom_task表的重试相关字段
            if custom_task_needs_migration:
                app.logger.info("正在添加custom_task表的重试相关字段...")
                db.session.execute(text("ALTER TABLE custom_task ADD COLUMN max_retries INTEGER DEFAULT 3"))
                db.session.execute(text("ALTER TABLE custom_task ADD COLUMN retry_delay INTEGER DEFAULT 300"))
                db.session.execute(text("ALTER TABLE custom_task ADD COLUMN retry_count INTEGER DEFAULT 0"))
                db.session.execute(text("ALTER TABLE custom_task ADD COLUMN last_error TEXT"))
                app.logger.info("✅ custom_task表字段添加完成")
            
            db.session.commit()
            
            app.logger.info("数据库迁移应用完成！已添加重试相关字段到相关表")
            
            return True
            
    except Exception as e:
        app.logger.error(f"迁移应用失败: {e}")
        if 'db' in locals():
            db.session.rollback()
        return False

if __name__ == '__main__':
    # 注册信号处理器
    signal.signal(signal.SIGINT, signal_handler)
    signal.signal(signal.SIGTERM, signal_handler)
    
    # 初始化数据库
    with app.app_context():
        # 应用迁移
        
        if not apply_retry_fields_migration():
            app.logger.error("应用迁移失败，服务无法启动")
            sys.exit(1)
        
        db.create_all()
    
    # 初始化调度器
    init_schedulers()
    
    # 加载任务
    load_tasks()
    
    # 启动调度器
    start_schedulers()
    
    app.logger.info("调度器服务启动完成")
    
    # 启动Flask应用
    app.run(host='0.0.0.0', port=5001, debug=False)